Deno Fresh のアイランド
Fresh のアイランド
インタラクティブアイランド
アイランドは、Freshにおけるクライアントサイドのインタラクティブ性を可能にします。アイランドは、サーバー上でレンダリングされ、クライアント上でハイドレーションされる孤立したPreactコンポーネントです。これは、通常サーバ上でのみレンダリングされるため、Freshの他のすべてのコンポーネントとは異なります。
アイランドは、Freshプロジェクトのislands/フォルダにファイルを作成することで定義されます。このファイルの名前は、PascalCaseまたはkebab-caseの島の名前でなければなりません。
import { useSignal } from "@preact/signals";
export default function MyIsland() {
const count = useSignal(0);
return (
<div>
Counter is at {count}.{" "}
<button onClick={() => (count.value += 1)}>+</button>
</div>
);
}
アイランドは、通常のPreactコンポーネントのようにページ内で使用することができます。Freshは、クライアント上で自動的にアイランドを再ハイドレーティングします。
ハイドレーションとは 静的なウェブページを動的なものに変換すること。
import MyIsland from "../islands/my-island.tsx";
export default function Home() {
return <MyIsland />;
}
JSXをアイランドに渡す方法
アイランドでは、children
プロパティを介してJSX要素を渡すことがサポートされています。
import { useSignal } from "@preact/signals";
import { ComponentChildren } from "preact";
interface Props {
children: ComponentChildren;
}
export default function MyIsland({ children }: Props) {
const count = useSignal(0);
return (
<div>
カウンターの値は {count}.{" "}
<button onClick={() => (count.value += 1)}>+</button>
{children}
</div>
);
}
これにより、サーバーでレンダリングされた静的なコンテンツをブラウザ上のアイランドに渡すことができます。
import MyIsland from "../islands/my-island.tsx";
export default function Home() {
return (
<MyIsland>
<p>このテキストはサーバーでレンダリングされています</p>
</MyIsland>
);
}
また、components/
ディレクトリに共有コンポーネントを作成し、静的コンテンツやインタラクティブなアイランドの両方で使用できます。これらのコンポーネントがアイランド内で使用されると、onClick
ハンドラーのようなインタラクティブ性を追加できます(アイランドの外でonClick
ハンドラーを使うと、動作しません)。
import { useSignal } from "@preact/signals";
import { ComponentChildren } from "preact";
import Card from "../components/Card.tsx";
import { Button } from "../components/Button.tsx";
interface Props {
children: ComponentChildren;
}
export default function MyIsland({ children }: Props) {
const count = useSignal(0);
return (
<Card>
カウンターの値は {count}.{" "}
<Button onClick={() => (count.value += 1)}>+</Button>
{children}
</Card>
);
}
import { ComponentChildren } from "preact";
interface CardProps {
children: ComponentChildren;
}
export default function Card({ children }: CardProps) {
return (
<div style={cardStyle}>
{children}
</div>
);
}
const cardStyle = {
padding: "16px",
border: "1px solid #ddd",
borderRadius: "8px",
boxShadow: "0 4px 8px rgba(0, 0, 0, 0.1)",
backgroundColor: "#fff",
};
アイランドに他のプロパティを渡す
アイランドにプロパティを渡すことはサポートされていますが、プロパティはシリアライズ可能なものである必要があります。Freshでは次のタイプの値をシリアライズできます:
- プリミティブ型(string、boolean、bigint、null)
- ほとんどの数値(Infinity、-Infinity、NaNは自動的にnullに変換)
- 文字列キーとシリアライズ可能な値を持つプレーンオブジェクト
- シリアライズ可能な値を含む配列
- Uint8Array
- JSX要素(
props.children
に制限) - Preact Signals(内部の値がシリアライズ可能である場合)
循環参照もサポートされています。オブジェクトやシグナルが複数回参照される場合、それは一度だけシリアライズされ、逆シリアル化時に参照が復元されます。Date
やカスタムクラス、関数などの複雑なオブジェクトはサポートされていません。
サーバーサイドレンダリング時には、FreshはHTMLに特別なコメントを付けて各アイランドの配置位置を示します。これにより、クライアント側に送信されるコードがインタラクティブなアイランドの静的な子要素のためにハイドレーションを必要とせず、アイランドを正しい場所に配置するために必要な情報を持つことができます。インタラクティブ性が必要ない場合、JavaScriptはクライアントに送信されません。
<!--frsh-myisland_default:default:0-->
<div>
カウンターの値は 0.
<button>+</button>
<!--frsh-slot-myisland_default:children-->
<p>このテキストはサーバーでレンダリングされています</p>
<!--/frsh-slot-myisland_default:children-->
</div>
<!--/frsh-myisland_default:default:0-->
アイランドのネスト
アイランドは他のアイランド内にネストすることもできます。この場合、通常のPreactコンポーネントとして動作しますが、シリアライズされたプロパティが存在する場合、それらを受け取ります。
import { useSignal } from "@preact/signals";
import { ComponentChildren } from "preact";
interface Props {
children: ComponentChildren;
foo: string;
}
function randomNumber() {
return Math.floor(Math.random() * 100);
}
export default function MyIsland({ children, foo }: Props) {
const number = useSignal(randomNumber());
return (
<div>
<p>プロパティからの文字列: {foo}</p>
<p>
<button onClick={() => (number.value = randomNumber())}>ランダム</button>
数値は: {number}.
</p>
</div>
);
}
つまり、Freshでは、アプリに最適な方法で静的な部分とインタラクティブな部分を組み合わせることができます。必要なJavaScriptだけがブラウザに送信されます。
クライアントのみでアイランドをレンダリング
EventSource
やnavigator.getUserMedia
などのクライアント専用APIを使用すると、このコンポーネントはサーバーで実行されず、次のようなエラーが発生します:
ルート処理またはページレンダリング中にエラーが発生しました。ReferenceError: EventSource is not defined at Object.MyIsland (file:///Users/someuser/fresh-project/islandsmy-island.tsx:6:18) at m (https://esm.sh/v129/preact-render-to-string@6.2.0/X-ZS8q/denonext/preact-render-to-string.mjs:2:2602) at m (https://esm.sh/v129/preact-render-to-string@6.2.0/X-ZS8q/denonext/preact-render-to-string.mjs:2:2113) ....
この問題を解決するには、IS_BROWSER
フラグをガードとして使用します
import { IS_BROWSER } from "$fresh/runtime.ts";
export function MyIsland() {
// アイランドに適した事前レンダリング可能なJSXを返す
if (!IS_BROWSER) return <div></div>;
// ブラウザで実行する必要があるコードをここに記述
// 例:EventSource, navigator.getUserMedia など
return <div></div>;
}
